// Package convert 结构体转换为map[string]string
// NOTE 2024-05-11
package convert

import (
	"encoding/json"
	"errors"
	"fmt"
	"reflect"
	"strconv"
	"strings"
)

// StructToStruct 是一个万能转换函数，它可以将一个结构体转换为另一个结构体
func StructToStruct[T any](src any, fieldMap map[string]string) (T, error) {
	// 声明一个空映射
	mapping := make(map[string]string)
	// 如果映射不为空，则更新映射
	if fieldMap != nil {
		mapping = fieldMap
	}
	var dst T

	// 检查src是否为结构体
	srcValue := reflect.ValueOf(src)
	srcType := srcValue.Type()
	if srcType.Kind() != reflect.Struct {
		return dst, errors.New("source is not a struct")
	}

	// 创建目标类型的实例
	dstPtr := reflect.New(reflect.TypeOf(dst))
	dstValue := dstPtr.Elem()

	// 遍历源结构体的字段
	for i := 0; i < srcType.NumField(); i++ {
		srcField := srcType.Field(i)
		srcFieldValue := srcValue.Field(i)

		// 根据映射找到目标结构体中的字段名
		dstFieldName, ok := mapping[srcField.Name]
		if !ok {
			dstFieldName = srcField.Name // 假定字段名相同
		}

		// 在目标结构体中寻找对应的字段
		if dstField := dstValue.FieldByName(dstFieldName); dstField.IsValid() && dstField.CanSet() {
			// 源字段和目标字段类型一致，则直接赋值
			if dstField.Type() == srcFieldValue.Type() {
				dstField.Set(srcFieldValue)
				continue
			}
			// 源字段和目录字段不一致，调用函数进行转换
			if err := AnyToAny(srcFieldValue.Interface(), dstField.Addr().Interface()); err != nil {
				return dst, fmt.Errorf("failed to convert field %s: %w", srcField.Name, err)
			}
		}
	}

	// 将目标结构体的值转换为目标类型
	dst = dstPtr.Elem().Interface().(T)

	return dst, nil
}

func StructSliceToStructSlice[T any](src any, fieldMap map[string]string) ([]T, error) {
	srcValue := reflect.ValueOf(src)
	srcType := srcValue.Type()
	if srcType.Kind() != reflect.Slice {
		return nil, errors.New("source is not a slice")
	}

	dstSlice := make([]T, srcValue.Len())
	for i := 0; i < srcValue.Len(); i++ {
		srcStruct := srcValue.Index(i).Interface()
		dstStruct, err := StructToStruct[T](srcStruct, fieldMap)
		if err != nil {
			return nil, err
		}
		dstSlice[i] = dstStruct
	}

	return dstSlice, nil
}

// StructToMap 将结构体转换为map[string]string
// 使用json标签作为键名，如果无json标签则使用字段名
// 如果结构体中有嵌套结构体，会将嵌套结构体的字段展平
func StructToMap(s interface{}) (map[string]string, error) {
	val := reflect.ValueOf(s)
	if val.Kind() == reflect.Ptr {
		val = val.Elem()
	}

	if val.Kind() != reflect.Struct {
		return nil, fmt.Errorf("input is not a struct")
	}

	mapResult := make(map[string]string)
	return structToMapRecursive(val, mapResult)
}

// structToMapRecursive 是一个递归辅助函数，用来处理嵌套结构体。
func structToMapRecursive(val reflect.Value, mapResult map[string]string) (map[string]string, error) {
	typ := val.Type()
	for i := 0; i < val.NumField(); i++ {
		field := val.Field(i)
		fieldType := typ.Field(i)

		// 检查字段是否是匿名的
		if fieldType.Anonymous {
			// 递归处理匿名字段（展平）
			internalMap, err := structToMapRecursive(field, mapResult)
			if err != nil {
				return nil, err
			}
			for k, v := range internalMap {
				mapResult[k] = v
			}
		} else {
			jsonTag := fieldType.Tag.Get("json")
			keyName := jsonTag

			// 如果json标签为空或者为"-", 使用字段名作为键名
			if jsonTag == "" || jsonTag == "-" {
				keyName = fieldType.Name
			} else {
				// 如果json标签中包含逗号，如`json:"name,omitempty"`，只取逗号前的部分作为键名
				if commaIdx := strings.Index(jsonTag, ","); commaIdx != -1 {
					keyName = jsonTag[:commaIdx]
				}
			}

			// 检查字段值是否可以转换为string
			switch field.Kind() {
			case reflect.String:
				mapResult[keyName] = field.String()
			case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
				mapResult[keyName] = strconv.FormatInt(field.Int(), 10)
			case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
				mapResult[keyName] = strconv.FormatUint(field.Uint(), 10)
			case reflect.Bool:
				mapResult[keyName] = strconv.FormatBool(field.Bool())
			case reflect.Struct, reflect.Slice:
				// 如果不是匿名结构体，将使用原始方式序列化为JSON字符串
				b, err := json.Marshal(field.Interface())
				if err != nil {
					return nil, fmt.Errorf("unable to json marshal field %s: %v", fieldType.Name, err)
				}
				mapResult[keyName] = string(b)
			default:
				return nil, fmt.Errorf("field %s is not convertable to string", fieldType.Name)
			}
		}
	}

	return mapResult, nil
}
